/*
 * TurnServer, the OpenSource Java Solution for TURN protocol. Maintained by the
 * Jitsi community (http://jitsi.org).
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jitsi.turnserver.listeners;

import java.util.logging.*;

import org.ice4j.*;
import org.ice4j.attribute.*;
import org.ice4j.message.*;
import org.ice4j.stack.*;
import org.jitsi.turnserver.stack.*;

/**
 * The class that would be handling and responding to incoming ChannelBind
 * requests that are validated and sends a success or error response
 * 
 * @author Aakash Garg
 */
public class ChannelBindRequestListener
    implements RequestListener
{

    /**
     * The <tt>Logger</tt> used by the <tt>ChannelBindRequestListener</tt> class
     * and its instances for logging output.
     */
    private static final Logger logger = Logger
        .getLogger(ChannelBindRequestListener.class.getName());

    private final TurnStack turnStack;

    /**
     * The indicator which determines whether this
     * <tt>ChannelBindrequestListener</tt> is currently started.
     */
    private boolean started = false;

    /**
     * Creates a new ChannelBindRequestListener
     * 
     * @param turnStack
     */
    public ChannelBindRequestListener(StunStack turnStack)
    {
        if (turnStack instanceof TurnStack)
        {
            this.turnStack = (TurnStack) turnStack;
        }
        else
        {
            throw new IllegalArgumentException("This is not a TurnStack!");
        }
    }

    @Override
    public void processRequest(StunMessageEvent evt)
        throws IllegalArgumentException
    {
        if (logger.isLoggable(Level.FINER))
        {
            logger.setLevel(Level.FINEST);
//            logger.finer("Received request " + evt);
        }
        
        Message message = evt.getMessage();
        if (message.getMessageType() == Message.CHANNELBIND_REQUEST)
        {
            logger.finest("Received Channel Bind request ");
            logger.finest("Event tran : "+evt.getTransactionID());
            
            Response response = null;
 
            TransportAddress clientAddress = evt.getRemoteAddress();
            TransportAddress serverAddress = evt.getLocalAddress();
            Transport transport = serverAddress.getTransport();
            FiveTuple fiveTuple =
                new FiveTuple(clientAddress, serverAddress, transport);
            
            Allocation allocation 
                = this.turnStack.getServerAllocation(fiveTuple);
            
            ChannelNumberAttribute channelNo =
                (ChannelNumberAttribute) message.getAttribute(
                    Attribute.CHANNEL_NUMBER);
            XorPeerAddressAttribute xorPeerAddress =
                (XorPeerAddressAttribute) message
                    .getAttribute(Attribute.XOR_PEER_ADDRESS);
            if(xorPeerAddress!=null)
            {
                xorPeerAddress.setAddress(xorPeerAddress.getAddress(),
                    evt.getTransactionID().getBytes());
            }
            
            logger.finest("Adding ChannelBind : "
                                    +(int)(channelNo.getChannelNumber())
                                    +", "+xorPeerAddress.getAddress());
            ChannelBind channelBind = new ChannelBind(
                                            xorPeerAddress.getAddress(),
                                            channelNo.getChannelNumber());

            Character errorCode = null;
            if(channelNo==null || xorPeerAddress==null)
            {
                errorCode = ErrorCodeAttribute.BAD_REQUEST;
            }
            else if(!ChannelNumberAttribute.isValidRange(
                channelNo.getChannelNumber()))
            {
                errorCode = ErrorCodeAttribute.BAD_REQUEST;
            }
            else if (allocation == null
                || allocation.isBadChannelRequest(channelBind))
            {
                errorCode = ErrorCodeAttribute.BAD_REQUEST;
            }
            else if(!TurnStack.isIPAllowed(xorPeerAddress.getAddress()))
            {
                errorCode = ErrorCodeAttribute.FORBIDDEN;
            }
            else if(!allocation.canHaveMoreChannels())
            {
                errorCode = ErrorCodeAttribute.INSUFFICIENT_CAPACITY;
            }
            
            if(errorCode != null)
            {
                logger.finest("Creating ChannelBindError response : "
                                        +(int)errorCode);
                response  
                    = MessageFactory.createChannelBindErrorResponse(errorCode);
            }
            else
            {
                logger.finest("Creating ChannelBind sucess response");
                try
                {
                    logger.finer("Adding ChannelBind : "+channelBind);
                    allocation.addChannelBind(channelBind);
                }
                catch(IllegalArgumentException ex)
                {
                    logger.log(Level.FINEST,ex.getMessage());
                }
                response = MessageFactory.createChannelBindResponse();
            }
            
            try
            {
                turnStack.sendResponse(evt.getTransactionID().getBytes(),
                    response, evt.getLocalAddress(), evt.getRemoteAddress());
            }
            catch (Exception e)
            {
                logger.log(Level.INFO, "Failed to send " + response
                    + " through " + evt.getLocalAddress(), e);
                // try to trigger a 500 response although if this one failed,
                throw new RuntimeException("Failed to send a response", e);
            }
        }
        else
        {
            return;
        }
    }

    /**
     * Starts this <tt>ChannelBindRequestListener</tt>. If it is not currently
     * running, does nothing.
     */
    public void start()
    {
        if (!started)
        {
            turnStack.addRequestListener(this);
            started = true;
        }
    }

    /**
     * Stops this <tt>ChannelBindRequestListenerr</tt>. A stopped
     * <tt>ChannelBindRequestListenerr</tt> can be restarted by calling
     * {@link #start()} on it.
     */
    public void stop()
    {
        turnStack.removeRequestListener(this);
        started = false;
    }
}
